﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace XOS_VHD_FAT32
{
    [StructLayout(LayoutKind.Sequential, Size = 32, Pack = 1)]
    struct LFN
    {
        //0x00	1	Sequence Number (bit 6: last logical, first physical LFN entry, bit 5: 0; bits 4-0: number 0x01..0x14 (0x1F), deleted entry: 0xE5)
        public byte SeqNumber;
        //0x01	10	Name characters (five UTF-16 characters)
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
        private byte[] _name_p1;
        //0x0B	1	Attributes (always 0x0F)
        private byte _flag1;
        //0x0C	1	Type (always 0x00 for VFAT LFN, other values reserved for future use; for special usage of bits 4 and 3 in SFNs see below)
        private byte _flag2;
        //0x0D	1	Checksum of DOS file name
        public byte _checksum;
        //0x0E	12	Name characters (six UTF-16 characters)
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
        private byte[] _name_p2;
        //0x1A	2	First cluster (always 0x0000)
        private short _flag3;
        //0x1C	4	Name characters (two UTF-16 characters)
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
        private byte[] _name_p3;

        public static IEnumerable<LFN> GetLFNEntries(string name, string shortName)
        {
            var items = GetLFNEntriesInternal(name, shortName).OrderByDescending(t =>
            {
                t.SeqNumber &= 0x3f;
                return t.SeqNumber;
            }).ToArray();

            if (items.Length > 0)
            {
                items[0].SeqNumber |= 0x40;
            }

            return items;
        }

        private static IEnumerable<LFN> GetLFNEntriesInternal(string name, string shortName)
        {
            if (shortName.Length > 12)
            {
                throw new System.IO.PathTooLongException();
            }

            if (name.Length > 200)
            {
                throw new System.IO.PathTooLongException();
            }

            var cs = DirEntry.lfn_checksum(shortName);
            var seqNumber = 0;

            name += '\0';

            foreach (var buff in GetNameParts(name))
            {
                var part = buff;

                seqNumber++;

                var lfn = new LFN
                {
                    _flag1 = 0x0f,
                    _flag2 = 0,
                    _flag3 = 0,
                    _checksum = cs,
                    SeqNumber = (byte)(seqNumber & 0x3f),
                    _name_p1 = new byte[10],
                    _name_p2 = new byte[12],
                    _name_p3 = new byte[4],
                };

                if (part.Length < 26)
                {
                    Array.Resize(ref part, 26);
                    lfn.SeqNumber |= 0x40;
                }

                Array.Copy(part, 0, lfn._name_p1, 0, 10);
                Array.Copy(part, 10, lfn._name_p2, 0, 12);
                Array.Copy(part, 22, lfn._name_p3, 0, 4);

                yield return lfn;
            }
        }

        private static IEnumerable<byte[]> GetNameParts(string name)
        {
            var bytes = Encoding.Unicode.GetBytes(name);

            for (int i = 0; i < bytes.Length; i += 26)
            {
                var bytesToCopy = Math.Min(26, bytes.Length - i);

                var data = new byte[bytesToCopy];

                Array.Copy(bytes, i, data, 0, data.Length);

                yield return data;
            }
        }

        public bool IsValid
        {
            get
            {
                return _flag1 == 0x0f && _flag2 == 0 && _flag3 == 0 && (SeqNumber & 0x80) == 0;
            }
        }

        public bool IsLastEntry
        {
            get
            {
                return (SeqNumber & 0x40) == 0x40;
            }
        }

        public string NamePart
        {
            get
            {
                var str = Encoding.Unicode.GetString(_name_p1.Concat(_name_p2.Concat(_name_p3)).ToArray());

                var id = str.IndexOf('\0');

                return id == -1 ? str : str.Substring(0, id);
            }
        }

        public override string ToString()
        {
            return "<"+NamePart+">";
        }
    }

    class LFNBuilder
    {
        private readonly List<LFN> _parts = new List<LFN>();

        public void Clear()
        {
            _parts.Clear();
        }

        public void Append(LFN entry)
        {
            // 0x7f

            if (!entry.IsValid ||
                !_parts.All(t => t._checksum == entry._checksum) ||
                _parts.Count > 0 && entry.IsLastEntry)
            {
                _parts.Clear();
            }

            if (_parts.Count == 0)
            {
                if (entry.IsLastEntry)
                {
                    entry.SeqNumber &= 0x3F;
                }
                else
                {
                    // invalid LFN entry
                    return;
                }
            }

            _parts.Add(entry);
        }

        public string Name
        {
            get
            {
                if (!IsValid)
                {
                    return null;
                }

                var sb = new StringBuilder();
                foreach (var part in _parts.OrderBy(t => t.SeqNumber).Select(t => t.NamePart))
                {
                    sb.Append(part);
                }
                return sb.ToString();
                //
            }
        }

        public bool IsValid
        {
            get
            {
                return _parts.Count > 0;
            }
        }

        internal bool ValidateDirEntry(DirEntry entry)
        {
            return IsValid && entry.lfn_checksum() == _parts.First()._checksum;
        }

        public override string ToString()
        {
            return Name;
        }
    }
}
